fix: wrap insertEvent with withRetry for concurrent PostToolUse hooks#243
Closed
james-gough-op wants to merge 14 commits intomksglu:nextfrom
Closed
fix: wrap insertEvent with withRetry for concurrent PostToolUse hooks#243james-gough-op wants to merge 14 commits intomksglu:nextfrom
james-gough-op wants to merge 14 commits intomksglu:nextfrom
Conversation
When many tool calls complete in parallel (e.g., batch Linear get_issue), Claude Code spawns multiple PostToolUse hooks simultaneously. These all open the same per-project SessionDB and compete for the SQLite write lock. While better-sqlite3's busy_timeout (30s) handles most contention, edge cases during transaction lock escalation can still surface SQLITE_BUSY. This causes hooks to fail, and Claude Code reports "hook error" to users. Changes: - Wrap SessionDB.insertEvent() transaction with withRetry() for defense-in-depth against SQLITE_BUSY under concurrent hook access - Set busy_timeout pragma in BunSQLiteAdapter to match better-sqlite3's timeout option (previously ignored, causing immediate SQLITE_BUSY failures under Bun runtime) - Add concurrent insert test verifying multiple DB instances can write to the same file without data loss
18 tasks
mksglu
added a commit
that referenced
this pull request
Apr 13, 2026
…#243) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Owner
|
Hey @james-gough-op — great work here. Reviewed and merged onto What we validated:
Note: We resolved the merge conflict with Commit: Thanks for the thorough analysis of the lock escalation scenario — the transaction retry approach is exactly right. 🙏 |
sebastianbreguel
added a commit
to sebastianbreguel/context-mode
that referenced
this pull request
Apr 15, 2026
…p paths Adds unit tests for pure utilities in src/db-base.ts that back the SQLITE_BUSY retry wrapping (mksglu#263, mksglu#243) and corruption recovery path behind mksglu#244/mksglu#218 incidents. - withRetry: first-try success, retry-on-busy, rethrow non-busy, empty delays, exhaustion message, non-Error throws, delay honored. - isSQLiteCorruptionError: all four documented signatures + negatives. - renameCorruptDB: main-only, all sidecars, missing files. - cleanOrphanedWALFiles: orphan removed, live DB preserved. - deleteDBFiles: unconditional cleanup of main/-wal/-shm. - defaultDBPath: pid embedding, prefix, tmpdir scoping. 31 tests, all green. No changes to src/.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / Why / How
What: Wrap
SessionDB.insertEvent()withwithRetry()and propagatebusy_timeoutpragma to Bun's SQLite adapter.Why: When many tool calls complete in parallel (e.g., 16 batch
mcp__linear__get_issuecalls during a standup summary), Claude Code spawns multiple PostToolUse hooks simultaneously. These all open the same per-project SessionDB and compete for the SQLite write lock. While better-sqlite3'sbusy_timeout(30s) handles most contention, edge cases during transaction lock escalation can surfaceSQLITE_BUSY, causing Claude Code to reportPostToolUse:mcp__linear__get_issue hook errorto users.Additionally, the
BunSQLiteAdapterfactory was silently dropping thetimeoutoption passed fromSQLiteBase, meaning Bun runtime had nobusy_timeoutat all — concurrent writes would fail immediately withSQLITE_BUSY.How:
src/session/db.ts— Changedtransaction()tothis.withRetry(() => transaction())ininsertEvent, leveraging the existing retry infrastructure inSQLiteBasesrc/db-base.ts— Addedbusy_timeoutpragma inBunDatabaseFactorywhenopts.timeoutis provided, matching better-sqlite3's automatic behaviorAffected platforms
(The Bun
busy_timeoutfix applies to any platform using Bun runtime for the MCP server. ThewithRetryfix applies to all platforms since PostToolUse hooks run with Node.js.)Test plan
Concurrent Insert Resiliencetest intests/session/session-db.test.ts— opens 5SessionDBinstances against the same DB file and inserts events from each, verifying all 5 events are stored without errorsnpm run typecheckpasses cleanChecklist
npm testpasses (6 pre-existing failures unrelated to this change — security, cursor-hooks, vscode-hooks, session-hooks-smoke, hooks/integration)npm run typecheckpassesnextbranch (unless hotfix)